Spring IoC基础
# Spring IoC
在Spring 中,它会认为一切Java 类都是资源,而资源都是Bean,容纳这些Bean 的是Spring 所提供的IoC 容器,所以Spring 是一种基于Bean的编程。
为了实现一个功能,你需要调用一个接口会对象的方法,在潜意识里你会觉得对象应该由你主动创建,但是事实上这并不是你真实的需要,因为也许你对某一领域并不精通,这个时候可以把创建对象的主动权转交给别人,这就是控制反转的概念。
控制反转是一种通过描述(在Java中可以是XML 或者注解)并通过第三方去产生或获取特定对象的方式。在Spring 中实现控制反转的是IoC 容器,其实现方法是依赖注入( DI )。
它最大的好处在于降低对象之间的耦合,在一个系统中有些类,具体如何实现并不需要去理解,只需要知道它有什么用就可以了。只是这里对象的产生依靠于IoC 容器,而不是开发者主动的行为。
# IoC配置的三种方式
# xml 配置
顾名思义,就是将bean的信息配置.xml文件里,通过Spring加载文件为我们创建bean。这种方式出现很多早前的SSM项目中,将第三方类库或者一些配置工具类都以这种方式进行配置,主要原因是由于第三方类不支持Spring注解。
- 优点: 可以使用于任何场景,结构清晰,通俗易懂
- 缺点: 配置繁琐,不易维护,枯燥无味,扩展性差
举例:
- 配置xx.xml文件
- 声明命名空间和配置bean
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="userService" class="tech.pdai.springframework.service.UserServiceImpl">
<property name="userDao" ref="userDao"/>
</bean>
</beans>
# Java 配置
将类的创建交给我们配置的JavcConfig类来完成,Spring只负责维护和管理,采用纯Java创建方式。其本质上就是把在XML上的配置声明转移到Java配置类中
- 优点:适用于任何场景,配置方便,因为是纯Java代码,扩展性高,十分灵活
- 缺点:由于是采用Java类的方式,声明不明显,如果大量配置,可读性比较差
举例:
- 创建一个配置类, 添加@Configuration注解声明为配置类
- 创建方法,方法上加上@bean,该方法用于创建实例并返回,该实例创建后会交给spring管理,方法名建议与实例名相同(首字母小写)。注:实例类不需要加任何注解
@Configuration
public class BeansConfig {
@Bean("userDao")
public UserDaoImpl userDao() {
return new UserDaoImpl();
}
@Bean("userService")
public UserServiceImpl userService() {
UserServiceImpl userService = new UserServiceImpl();
userService.setUserDao(userDao());
return userService;
}
}
# 注解配置
通过在类上加注解的方式,来声明一个类交给Spring管理,Spring会自动扫描带有@Component,@Controller,@Service,@Repository这四个注解的类,然后帮我们创建并管理,前提是需要先配置Spring的注解扫描器。
- 优点:开发便捷,通俗易懂,方便维护。
- 缺点:具有局限性,对于一些第三方资源,无法添加注解。只能采用XML或JavaConfig的方式配置
举例:
- 对类添加@Component相关的注解,比如@Controller,@Service,@Repository
- 设置ComponentScan的basePackage, 比如
<context:component-scan base-package='tech.pdai.springframework'>
, 或者@ComponentScan("tech.pdai.springframework")
注解,或者new AnnotationConfigApplicationContext("tech.pdai.springframework")
指定扫描的basePackage.
@Service
public class UserServiceImpl {
@Autowired
private UserDaoImpl userDao;
public List<User> findUserList() {
return userDao.findUserList();
}
}
# Spring IoC容器
Spring IoC 容器的设计主要是基于BeanFactory 和ApplicationContext 两个接口,其中ApplicationContext 是BeanFactory 的子接口之一,换句话说BeanFactory 是Spring IoC 容器所定义的最底层接口,而ApplicationContext 是其高级接口之一,并且对BeanFactory 功能做了许多有用的扩展,所以在绝大部分的工作场景下, 都会使用ApplicationContext 作为Spring IoC 容器。ApplicationContext用来对Bean做初始化工作,根据context也可以获取到bean的实例。
Bean的初始化过程
Resource 定位,这步是Spring IoC 容器根据开发者的配置,进行资源定位,在Spring的开发中,通过XML 或者注解都是十分常见的方式,定位的内容是由开发者所提供的。
BeanDefinition 的载入,这个过程就是Spring 根据开发者的配置获取对应的POJO,用以生成对应实例的过程。
BeanDefinition 的注册,这个步骤就相当于把之前通过BeanDefinition 载入的POJO往Spring IoC 容器中注册,这样就可以使得开发和测试人员都可以通过描述从中得到Spring IoC 容器的Bean 了。
# 依赖注入
就 IOC 本身而言,其并不是什么新技术,只是一种思想理念。IOC 的核心就是原先创建一个对象,我们需要自己直接通过 new 来创建,而 IOC 就相当于有人帮们创建好了对象,需要使用的时候直接去拿就行,IOC 主要有两种实现方式:
DL(Dependency Lookup):依赖查找。==这种就是说容器帮我们创建好了对象,我们需要使用的时候自己再主动去容器中查找==,如:
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/application-context.xml");
Object bean = applicationContext.getBean("object");
DI(Dependency Inject):依赖注入。依赖注入相比较依赖查找又是一种优化,也就是我们==不需要自己去查找,只需要告诉容器当前需要注入的对象,容器就会自动将创建好的对象进行注入(赋值)==。
Bean的初始化结束之后,还需要完成依赖注入。如果没有完成依赖注入,也就是没有注入其配置的资源给Bean ,那么它还不能完全使用。
对于依赖注入, Spring Bean还有一个配置选项一一lazy-init(延迟注入) , 其含义就是是否初始化Spring Bean 。在没有任何配置的情况下,它的默认值为default ,实际值为false ,也就是Spring IoC 默认会自动初始化Bean 。如果将其设置为true ,那么只有当我们使用Spring IoC 容器的getBean 方法获取它时,它才会进行初始化, 完成依赖注入。
# 依赖注入的方式
# setter方式
- 在XML配置方式中,property都是setter方式注入,比如下面的xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="userService" class="tech.pdai.springframework.service.UserServiceImpl">
<property name="userDao" ref="userDao"/>
</bean>
</beans>
本质上包含两步:
- 第一步,需要new UserServiceImpl()创建对象, 所以需要默认构造函数
- 第二步,调用setUserDao()函数注入userDao的值, 所以需要setUserDao()函数
所以对应的service类是这样的:
public class UserServiceImpl {
private UserDaoImpl userDao;
public UserServiceImpl() {
}
public List<User> findUserList() {
return this.userDao.findUserList();
}
public void setUserDao(UserDaoImpl userDao) {
this.userDao = userDao;
}
}
- 在注解和Java配置方式下
@Service
public class UserServiceImpl {
private UserDaoImpl userDao;
public List<User> findUserList() {
return this.userDao.findUserList();
}
@Autowired
public void setUserDao(UserDaoImpl userDao) {
this.userDao = userDao;
}
}
在Spring3.x刚推出的时候,推荐使用注入的就是这种, 但是这种方式比较麻烦,所以在Spring4.x版本中推荐构造函数注入。
# 构造函数
- 在XML配置方式中,
<constructor-arg>
是通过构造函数参数注入,比如下面的xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="userService" class="tech.pdai.springframework.service.UserServiceImpl">
<constructor-arg name="userDao" ref="userDao"/>
</bean>
</beans>
本质上是new UserServiceImpl(userDao)创建对象, 所以对应的service类是这样的:
public class UserServiceImpl {
private final UserDaoImpl userDao;
public UserServiceImpl(UserDaoImpl userDaoImpl) {
this.userDao = userDaoImpl;
}
public List<User> findUserList() {
return this.userDao.findUserList();
}
}
- 在注解和Java配置方式下
@Service
public class UserServiceImpl {
private final UserDaoImpl userDao;
@Autowired
public UserServiceImpl(final UserDaoImpl userDaoImpl) {
this.userDao = userDaoImpl;
}
public List<User> findUserList() {
return this.userDao.findUserList();
}
}
在Spring4.x版本中推荐的注入方式就是这种, 具体原因看后续章节。
# 注解注入
以@Autowired(自动注入)注解注入为例,修饰符有三个属性:Constructor,byType,byName。==默认按照byType注入==。
constructor:通过构造方法进行自动注入,spring会匹配与构造方法参数类型一致的bean进行注入,如果有一个多参数的构造方法,一个只有一个参数的构造方法,在容器中查找到多个匹配多参数构造方法的bean,那么spring会优先将bean注入到多参数的构造方法中。
byName:被注入bean的id名必须与set方法后半截匹配,并且id名称的第一个单词首字母必须小写,这一点与手动set注入有点不同。
byType:查找所有的set方法,将符合符合参数类型的bean注入。
比如:
@Service
public class UserServiceImpl {
@Autowired
private UserDaoImpl userDao;
public List<User> findUserList() {
return userDao.findUserList();
}
}
- 构造器注入:构造器注入依赖于构造方法实现,而构造方法可以是有参数的或者是无参数的。在大部分的情况下,我们都是通过类的构造方法来创建类对象, Spring 也可以采用反射的方式,通过使用构造方法来完成注入,这就是构造器注入的原理。
- setter注入:setter 注入是Spring 中最主流的注入方式,它利用Java Bean 规范所定义的setter 方法来完成注入,灵活且可读性高。它消除了使用构造器注入时出现多个参数的可能性,首先可以把构造方法声明为无参数的,然后使用setter 注入为其设置对应的值,其实也是通过Java 反射技术得以实现的。
- 接口注入:有些时候资源并非来自于自身系统,而是来自于外界,比如数据库连接资源完全可以在Tomcat 下配置,然后通过刑DI 的形式去获取它,这样数据库连接资源是属于开发工程外的资源,这个时候我们可以采用接口注入的形式来获取它。
# 依赖注入的注解
- @Autowired
- @Resource
# 依赖倒置/控制反转/依赖注入
# 依赖倒置 DIP
依赖倒置(Dependency inversion principle,缩写为 DIP)是面向对象六大基本原则之一。它是指一种特定的解耦形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。
该原则规定:
- 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
- 抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。
上面这两句话很抽象,需要细细品味才能发现其中奥秘,如果暂时理解不了也没关系,下文会结合具体案例帮助大家理解。
# 控制反转 IOC
控制反转(Inversion of Control,缩写为 IOC)是面向对象编程中的一种设计原则,用来降低计算机代码之间的耦合度。是实现依赖倒置原则的一种代码设计思路。其中最常见的方式叫做依赖注入,还有一种方式叫依赖查找。
# 依赖注入 DI
依赖注入(Dependency Injection,缩写为 DI)是实现控制反转的一种方式。常用的依赖注入方法有 3 种:
- 接口注入:
- **构造函数注入:**指 IoC 容器使用构造方法注入被依赖的实例。基于构造器的 DI 通过调用带参数的构造方法实现,每个参数代表一个依赖。
- **属性注入:**指 IoC 容器使用 setter 方法注入被依赖的实例。通过调用无参构造器或无参 static 工厂方法实例化 bean 后,调用该 bean 的 setter 方法,即可实现基于 setter 的 DI。
# IoC与DI的关系
控制反转是通过依赖注入实现的,其实它们是同一个概念的不同角度描述。通俗来说就是IoC是设计思想,DI是实现方式。
DI—Dependency Injection,即依赖注入:组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。
我们来深入分析一下:
- 谁依赖于谁?
当然是应用程序依赖于IoC容器;
- 为什么需要依赖?
应用程序需要IoC容器来提供对象需要的外部资源;
- 谁注入谁?
很明显是IoC容器注入应用程序某个对象,应用程序依赖的对象;
- 注入了什么?
就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。
- IoC和DI由什么关系呢?
其实它们是同一个概念的不同角度描述,由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以2004年大师级人物Martin Fowler又给出了一个新的名字:“依赖注入”,相对IoC 而言,“依赖注入”明确描述了“被注入对象依赖IoC容器配置依赖对象”。通俗来说就是IoC是设计思想,DI是实现方式。
# 参考
https://pdai.tech/md/spring/spring-x-framework-ioc.html
# Spring IoC如何解决循环依赖问题?
Spring IOC只能解决属性注入之间的循环依赖问题,如果是构造器之间的循环依赖,只会抛出BeanCurrentlyInCreationException异常。
Spring使用了3个Map来保存Bean,俗称为三级依赖:
- singletonObjects 一级缓存,用于保存实例化、注入、初始化完成的bean实例,可以使用的。
- earlySingletonObjects 二级缓存,bean刚刚构造完成,但是还没有进行属性填充。
- singletonFactories 三级缓存,用于保存正在创建中的bean,以便于后面扩展有机会创建代理对象,此时的bean是没有完成属性填充的。
假设A类和B类相互依赖,A中有一个B类的属性,B中有一个A类的属性。那么在初始化A的Bean时,首先会依次去一级依赖,去二级依赖,三级依赖中去找,都没有就调用创建方法创建实例A,将A添加到三级依赖中,然后对A的属性进行依赖注入,填充属性时,发现B的Bean在各级依赖中都没有,就创建B的bean添加到三级依赖,然后对B的属性进行填充,填充B的属性A时,会从三级依赖中取出A,填充完放到二级依赖,然后对B进行初始化,初始化完成添加到一级依赖。B初始化完成后,将B从一级依赖中,填充到实例A,A可以进入到二级依赖,完全初始化完成后,A进入到一级依赖,供用户代码使用。
# @Autowired
# @Autowired原理
举例:@Autowired private BookService bookService;
1)先按照类型去容器中找到对应的组件;bookService = ioc.getBean(BookService.class)
① 找到一个:找到就赋值
② 没找到就报异常
③ 按照类型可以找到多个?找到多个如何装配上?
a、类型一样就按照变量名为ID继续匹配
Ⅰ、匹配上就装配
Ⅱ、没有匹配?报错
原因:因为我们是按照变量名作为id继续匹配的
解决:使用@Qualifier指定一个新的id,找到就匹配
# Bean有多个实现类的装配原理
# 现象1
@Autowired
private BookService bookService;
Autowired 为组件自动赋值,自动查找ioc容器,找到就赋值,查找的方式是按照类型寻找,有可能找到多个,比如说有一个继承于要查找的类,如果是多个就按照变量名作为id继续匹配。
举例说:查找到多个相同的类是BookService 和 BookServiceExtend,这 个 时候会根据你要查找的bookservice 来查找id,BookService 的id默认是bookservice,BookServiceExtend的id默认是bookServiceExtend,一查找就只有BookService匹配上了
如果现在把例子改成bookService2
@Autowired
private BookService bookService2;
Spring容器先查找BookService类,一找就找到两个类(BookService 和BookServiceExtend),然后查找id,发现都不匹配,会直接报错
这个时候诞生了@Qualifier注解,让spring不使用bookService2作为id去查找,而是使用一个比如说bookService去查找,作为新的id去查找。@Qualifier注解找不到就报错,找到就装配
@Qualifier("bookService")
@Autowired
private BookService bookService2;
可以在Autowired(request = false)默认是true,找不到就会报错,置为false后,找不到会置为null
@Qualifier("bookService")
@Autowired(request=false)
private BookService bookService2;
# 现象2
public interface IUser {
void say();
}
@Service
public class User1 implements IUser{
@Override
public void say() {
}
}
@Service
public class User2 implements IUser{
@Override
public void say() {
}
}
@Service
public class UserService {
@Autowired
private IUser user;
}
报错:testService1是单例的,却找到两个对象。
# @Autowire 和 @Resource 注解的区别
@Autowired功能虽说非常强大,但是也有些不足之处。比如:比如它跟spring强耦合了,如果换成了JFinal等其他框架,功能就会失效。而@Resource是JSR-250提供的,它是Java标准,绝大部分框架都支持。
除此之外,有些场景使用@Autowired无法满足的要求,改成@Resource却能解决问题。接下来,我们重点看看@Autowired和@Resource的区别。
- @Autowired默认按byType自动装配,而@Resource默认byName自动装配。
@Autowired只包含一个参数:required,表示是否开启自动准入,默认是true。而**@Resource包含七个参数,其中最重要的两个参数是:name 和 type**。
@Autowired如果要使用byName,需要使用@Qualifier一起配合。而@Resource如果指定了name,则用byName自动装配,如果指定了type,则用byType自动装配。
@Autowired能够用在:构造器、方法、参数、成员变量和注解上,而@Resource能用在:类、成员变量和方法上。
@Autowired是spring定义的注解,而@Resource是JSR-250定义的注解。
# @Autowired的装配顺序
# @Resource的装配顺序
如果同时指定了name和type:
如果指定了name:
如果指定了type:
如果既没有指定name,也没有指定type: